/** @import { Node } from 'esrap/languages/ts' */
/** @import * as ESTree from 'estree' */
/** @import { AST } from 'svelte/compiler' */

// @ts-check
import process from 'node:process';
import fs from 'node:fs';
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import * as esrap from 'esrap';
import ts from 'esrap/languages/ts';

const DIR = '../../documentation/docs/98-reference/.generated';

const watch = process.argv.includes('-w');

function run() {
	/** @type {Record<string, Record<string, { messages: string[], details: string | null }>>} */
	const messages = {};
	const seen = new Set();

	fs.rmSync(DIR, { force: true, recursive: true });
	fs.mkdirSync(DIR);

	for (const category of fs.readdirSync('messages')) {
		if (category.startsWith('.')) continue;

		messages[category] = {};

		for (const file of fs.readdirSync(`messages/${category}`)) {
			if (!file.endsWith('.md')) continue;

			const markdown = fs
				.readFileSync(`messages/${category}/${file}`, 'utf-8')
				.replace(/\r\n/g, '\n');

			const sorted = [];

			for (const match of markdown.matchAll(/## ([\w]+)\n\n([^]+?)(?=$|\n\n## )/g)) {
				const [_, code, text] = match;

				if (seen.has(code)) {
					throw new Error(`Duplicate message code ${category}/${code}`);
				}

				sorted.push({ code, _ });

				const sections = text.trim().split('\n\n');
				const details = [];

				while (!sections[sections.length - 1].startsWith('> ')) {
					details.unshift(/** @type {string} */ (sections.pop()));
				}

				if (sections.length === 0) {
					throw new Error('No message text');
				}

				seen.add(code);
				messages[category][code] = {
					messages: sections.map((section) => section.replace(/^> /gm, '').replace(/^>\n/gm, '\n')),
					details: details.join('\n\n')
				};
			}

			sorted.sort((a, b) => (a.code < b.code ? -1 : 1));

			fs.writeFileSync(
				`messages/${category}/${file}`,
				sorted.map((x) => x._.trim()).join('\n\n') + '\n'
			);
		}

		fs.writeFileSync(
			`${DIR}/${category}.md`,
			'<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->\n\n' +
				Object.entries(messages[category])
					.map(([code, { messages, details }]) => {
						const chunks = [
							`### ${code}`,
							...messages.map((message) => '```\n' + message + '\n```')
						];

						if (details) {
							chunks.push(details);
						}

						return chunks.join('\n\n');
					})
					.sort()
					.join('\n\n') +
				'\n'
		);
	}

	/**
	 * @param {string} name
	 * @param {string} dest
	 */
	function transform(name, dest) {
		const source = fs
			.readFileSync(new URL(`./templates/${name}.js`, import.meta.url), 'utf-8')
			.replace(/\r\n/g, '\n');

		/** @type {AST.JSComment[]} */
		const comments = [];

		let ast = /** @type {ESTree.Node} */ (
			/** @type {unknown} */ (
				acorn.parse(source, {
					ecmaVersion: 'latest',
					sourceType: 'module',
					locations: true,
					onComment: comments
				})
			)
		);

		comments.forEach((comment) => {
			if (comment.type === 'Block') {
				comment.value = comment.value.replace(/^\t+/gm, '');
			}
		});

		ast = walk(ast, null, {
			Identifier(node, context) {
				if (node.name === 'CODES') {
					/** @type {ESTree.ArrayExpression} */
					const array = {
						type: 'ArrayExpression',
						elements: Object.keys(messages[name]).map((code) => ({
							type: 'Literal',
							value: code
						}))
					};

					return array;
				}
			}
		});

		const body = /** @type {ESTree.Program} */ (ast).body;

		const category = messages[name];

		// find the `export function CODE` node
		const index = body.findIndex((node) => {
			if (
				node.type === 'ExportNamedDeclaration' &&
				node.declaration &&
				node.declaration.type === 'FunctionDeclaration'
			) {
				return node.declaration.id.name === 'CODE';
			}
		});

		if (index === -1) throw new Error(`missing export function CODE in ${name}.js`);

		const template_node = body[index];
		body.splice(index, 1);

		const jsdoc = /** @type {AST.JSComment} */ (
			comments.findLast((comment) => comment.start < /** @type {number} */ (template_node.start))
		);

		const printed = esrap.print(
			/** @type {Node} */ (ast),
			ts({
				comments: comments.filter((comment) => comment !== jsdoc)
			})
		);

		for (const code in category) {
			const { messages } = category[code];
			/** @type {string[]} */
			const vars = [];

			const group = messages.map((text, i) => {
				for (const match of text.matchAll(/%(\w+)%/g)) {
					const name = match[1];
					if (!vars.includes(name)) {
						vars.push(match[1]);
					}
				}

				return {
					text,
					vars: vars.slice()
				};
			});

			/** @type {ESTree.Expression} */
			let message = { type: 'Literal', value: '' };
			let prev_vars;

			for (let i = 0; i < group.length; i += 1) {
				const { text, vars } = group[i];

				if (vars.length === 0) {
					message = {
						type: 'Literal',
						value: text
					};
					prev_vars = vars;
					continue;
				}

				const parts = text.split(/(%\w+%)/);

				/** @type {ESTree.Expression[]} */
				const expressions = [];

				/** @type {ESTree.TemplateElement[]} */
				const quasis = [];

				for (let i = 0; i < parts.length; i += 1) {
					const part = parts[i];
					if (i % 2 === 0) {
						const str = part.replace(/(`|\${)/g, '\\$1');
						quasis.push({
							type: 'TemplateElement',
							value: { raw: str, cooked: str },
							tail: i === parts.length - 1
						});
					} else {
						expressions.push({
							type: 'Identifier',
							name: part.slice(1, -1)
						});
					}
				}

				/** @type {ESTree.Expression} */
				const expression = {
					type: 'TemplateLiteral',
					expressions,
					quasis
				};

				if (prev_vars) {
					if (vars.length === prev_vars.length) {
						throw new Error('Message overloads must have new parameters');
					}

					message = {
						type: 'ConditionalExpression',
						test: {
							type: 'Identifier',
							name: vars[prev_vars.length]
						},
						consequent: expression,
						alternate: message
					};
				} else {
					message = expression;
				}

				prev_vars = vars;
			}

			const clone = /** @type {ESTree.Statement} */ (
				walk(/** @type {ESTree.Node} */ (template_node), null, {
					FunctionDeclaration(node, context) {
						if (node.id.name !== 'CODE') return;

						const params = [];

						for (const param of node.params) {
							if (param.type === 'Identifier' && param.name === 'PARAMETER') {
								params.push(...vars.map((name) => ({ type: 'Identifier', name })));
							} else {
								params.push(param);
							}
						}

						return /** @type {ESTree.FunctionDeclaration} */ ({
							.../** @type {ESTree.FunctionDeclaration} */ (context.next()),
							params,
							id: {
								...node.id,
								name: code
							}
						});
					},
					TemplateLiteral(node, context) {
						/** @type {ESTree.TemplateElement} */
						let quasi = {
							type: 'TemplateElement',
							value: {
								...node.quasis[0].value
							},
							tail: node.quasis[0].tail
						};

						/** @type {ESTree.TemplateLiteral} */
						let out = {
							type: 'TemplateLiteral',
							quasis: [quasi],
							expressions: []
						};

						for (let i = 0; i < node.expressions.length; i += 1) {
							const q = structuredClone(node.quasis[i + 1]);
							const e = node.expressions[i];

							if (e.type === 'Literal' && e.value === 'CODE') {
								quasi.value.raw += code + q.value.raw;
								continue;
							}

							if (e.type === 'Identifier' && e.name === 'MESSAGE') {
								if (message.type === 'Literal') {
									const str = /** @type {string} */ (message.value).replace(/(`|\${)/g, '\\$1');
									quasi.value.raw += str + q.value.raw;
									continue;
								}

								if (message.type === 'TemplateLiteral') {
									const m = structuredClone(message);
									quasi.value.raw += m.quasis[0].value.raw;
									out.quasis.push(...m.quasis.slice(1));
									out.expressions.push(...m.expressions);
									quasi = m.quasis[m.quasis.length - 1];
									quasi.value.raw += q.value.raw;
									continue;
								}
							}

							out.quasis.push((quasi = q));
							out.expressions.push(/** @type {ESTree.Expression} */ (context.visit(e)));
						}

						return out;
					},
					Literal(node) {
						if (node.value === 'CODE') {
							return {
								type: 'Literal',
								value: code
							};
						}
					},
					Identifier(node) {
						if (node.name !== 'MESSAGE') return;
						return message;
					}
				})
			);

			const jsdoc_clone = {
				...jsdoc,
				value: /** @type {string} */ (jsdoc.value)
					.split('\n')
					.map((line) => {
						if (line === ' * MESSAGE') {
							return messages[messages.length - 1]
								.split('\n')
								.map((line) => ` * ${line}`)
								.join('\n');
						}

						if (line.includes('PARAMETER')) {
							return vars
								.map((name, i) => {
									const optional = i >= group[0].vars.length;

									return optional
										? ` * @param {string | undefined | null} [${name}]`
										: ` * @param {string} ${name}`;
								})
								.join('\n');
						}

						return line;
					})
					.filter((x) => x !== '')
					.join('\n')
			};

			const block = esrap.print(
				// @ts-expect-error some bullshit
				/** @type {ESTree.Program} */ ({ ...ast, body: [clone] }),
				ts({ comments: [jsdoc_clone] })
			).code;

			printed.code += `\n\n${block}`;

			body.push(clone);
		}

		fs.writeFileSync(
			dest,
			`/* This file is generated by scripts/process-messages/index.js. Do not edit! */\n\n` +
				printed.code,
			'utf-8'
		);
	}

	transform('compile-errors', 'src/compiler/errors.js');
	transform('compile-warnings', 'src/compiler/warnings.js');

	transform('client-warnings', 'src/internal/client/warnings.js');
	transform('client-errors', 'src/internal/client/errors.js');
	transform('server-warnings', 'src/internal/server/warnings.js');
	transform('server-errors', 'src/internal/server/errors.js');
	transform('shared-errors', 'src/internal/shared/errors.js');
	transform('shared-warnings', 'src/internal/shared/warnings.js');
}

if (watch) {
	let running = false;
	let timeout;

	fs.watch('messages', { recursive: true }, (type, file) => {
		if (running) {
			timeout ??= setTimeout(() => {
				running = false;
				timeout = null;
			});
		} else {
			running = true;

			// eslint-disable-next-line no-console
			console.log('Regenerating messages...');
			run();
		}
	});
}

run();
